/*
 * 
 * Copyright 2011 visual-analysis-of-cancer.
 * This file is part of visual-analysis-of-cancer.
 * 
 * visual-analysis-of-cancer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * visual-analysis-of-cancer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with kOSMik.  If not, see <http://www.gnu.org/licenses/>.
 *  
 * visual-analysis-of-cancer:
 * An tool to analye the WHO and GLOBOCAN cancer data.
 * http://code.google.com/p/visual-analysis-of-cancer/
 * 
 * 
 * 
 * Authors: Erik Sniegula, Johannes Dahlke
 * 
 * Module:  Programming in Processing for Visual Analytics
 * Group:   20
 * 
 * Created: Aug. 2011
 * Version: 1.0
 *
 */
 




package de.fuberlin.processing.who.cancer.gui.healthgraph;

import java.awt.Point;


import processing.core.PApplet;
import processing.core.PStyle;
import processing.core.PVector;


import controlP5.Button;
import controlP5.ControlP5;
import controlP5.Controller;
import controlP5.ControllerGroup;
import de.fuberlin.processing.who.cancer.controlp5ex.CustomTooltip;
import de.fuberlin.processing.who.cancer.data.DBException;
import de.fuberlin.processing.who.cancer.data.Database;
import de.fuberlin.processing.who.cancer.data.DatabaseConnector;
import de.fuberlin.processing.who.cancer.data.structs.StructAirPollutionExposureOfLocation;
import de.fuberlin.processing.who.cancer.data.structs.StructCancerCausesOfLocation;
import de.fuberlin.processing.who.cancer.data.structs.StructLifeExpectancyAndHealthFinancingAndCancerDataForLocation;
import de.fuberlin.processing.who.cancer.gui.HealthGraph;
import de.fuberlin.processing.who.cancer.interaction.CancerCauses;
import de.fuberlin.processing.who.cancer.utils.Graphics;
import de.fuberlin.processing.who.cancer.utils.Test;
import de.fuberlin.processing.who.cancer.utils.graphics.RingSegmentStyle;


public class CancerDisc extends Controller implements Orbit {

	private static float radiusScaleFactor = 100f;
	public static final float NUM_MIN_CIRCLE_RADIUS = 10.0f;

	static Orbit leftmost = null;
	static Orbit rightmost = null;
	static Orbit topmost = null;
	static Orbit bottommost = null;

	private float x;
	private float y;
	private float radius;
	private int locationId;
	private static DatabaseConnector db = null;
	private StructLifeExpectancyAndHealthFinancingAndCancerDataForLocation data;
	private StructCancerCausesOfLocation dataCancerCauses = new StructCancerCausesOfLocation();
	private static Integer dataMaxLifeExpectancy;
	private static Float dataMaxHealthFinancing;
	private static Integer dataMaxAirPollutionExposure;
	private static Integer dataMaxUVRadiationExposure;
	private static CancerCauses cancerCause = CancerCauses.AIR_POLLUTION;
	private ControlP5 controlP5;
	private ControllerGroup parent;
	private boolean isShowCancerDimensionArrows;

	private Rect nonscaledPrimalRect;


	public CancerDisc( final ControlP5 theControlP5,
			final ControllerGroup theParent, final int locationId) { // , final float
																																// theX, final
																																// float theY,
																																// final int
																																// theRadius) {
		super( theControlP5, theParent, "cancerDisc", 0, 0, 0, 0);// theX-theRadius,
																															// theY-theRadius,
																															// theRadius*2,
																															// theRadius*2);
		this.hide();
		this.controlP5 = theControlP5;
		this.parent = theParent;
		this.locationId = locationId;

		connectToDatabase();
		if ( preloadData()) {
			this._myName = this._myName + data.locationName;
		}
	}


	private static void connectToDatabase() {
		if ( Test.isUnassigned( db)) {
			try {
				db = Database.get();
				dataMaxLifeExpectancy = db.getMaxLifeExpenctancy();
				dataMaxHealthFinancing = db.getMaxHealthFinancing();
				dataMaxAirPollutionExposure = db.getMaxAirPollutionExposure();
				dataMaxUVRadiationExposure = db.getMaxUVRadiationExposure();
			} catch ( DBException e) {
				e.printStackTrace();
			}
		}
	}


	private boolean preloadData() {
		data = db
				.getLifeExpectancyAndHealthFinancingAndCancerDataForLocation( locationId);
		dataCancerCauses.airPollutionExposure = db
				.getAirPollutionExposureOfLocation( locationId);
		dataCancerCauses.uvRadiationExposure = db
				.getUVRadiationExposureOfLocation( locationId);
		dataCancerCauses.ageSpreading = db
				.getCancerIncidenceAgeSpreatingOfLocation( locationId);
		dataCancerCauses.sexSpreading = db
				.getCancerIncidenceSexSpreatingOfLocation( locationId);

		if ( Test.isAssigned( data)) {
			initNonscaledPrimalRect();
			return true;
		}
		return false;
	}


	public ControlP5 getControlP5() {
		return controlP5;
	}


	public ControllerGroup getParent() {
		return parent;
	}


	public String getLocationName() {
		return data.locationName;
	}


	public String getLinkForAdditinalInformations() {
		return String.format(
				"http://globocan.iarc.fr/factsheets/populations/factsheet.asp?uno=%d",
				data.populationId);
	}


	@Override
	public String getTooltipText() {
		String toolTipText = String.format(
				"Health financing: %.02f per GDP\nLife expectancy: %d years\nCancer incidence: %.2f ASR, %d total\nCancer mortality: %.2f ASR, %d total\n",
				data.healthFinancing, data.lifeExpectancy, data.incidenceASR, data.incidenceTotal, data.mortalityASR, data.mortalityTotal);
		switch ( cancerCause) {
			case AIR_POLLUTION:
				toolTipText += String.format( "Air pollution exposure: %d mg/m3\n",
						dataCancerCauses.airPollutionExposure.airPollution);
				break;
			case UV_RADIATION:
				toolTipText += String.format( "UV radiation exposure: %d\n",
						dataCancerCauses.uvRadiationExposure.uvRadiation);
				break;
			case AGE:
				toolTipText += String
						.format(
								"Cancer incidences in the aged population of:\n-  0-14: %.02f %%\n-  15-59: %.02f %%\n-  60 + : %.02f %%\n",
								dataCancerCauses.ageSpreading.percentageDistributionPerAgeClasses[0],
								dataCancerCauses.ageSpreading.percentageDistributionPerAgeClasses[1],
								dataCancerCauses.ageSpreading.percentageDistributionPerAgeClasses[2]);
				break;
			case SEX:
			default:
				toolTipText += String
						.format(
								"Cancer incidences by sex:\n-  male: %.02f %%\n-  female: %.02f %%\n-  total: %d\n",
								dataCancerCauses.sexSpreading.malesPer100,
								dataCancerCauses.sexSpreading.femalesPer100,
								dataCancerCauses.sexSpreading.total);
				break;
		}
		return toolTipText;
	}


	private void initNonscaledPrimalRect() {
		nonscaledPrimalRect = new Rect(
				(float) data.healthFinancing / (float) dataMaxHealthFinancing
						* (float) parent().getWidth(),
				(float) parent().getHeight()
						- ( (float) data.lifeExpectancy / (float) dataMaxLifeExpectancy * (float) parent()
								.getHeight()), data.incidenceASR, data.incidenceASR);
	}


	private float getRelativeHealthFinancing() {
		return (float) data.healthFinancing / (float) dataMaxHealthFinancing;
	}


	private float getRelativeLifeExpectancy() {
		return (float) data.healthFinancing / (float) dataMaxHealthFinancing;
	}


	public static void setCancerCause( CancerCauses cancerCause) {
		CancerDisc.cancerCause = cancerCause;
	}


	public Rect getPrimalRect() {
		if ( Test.isAssigned( data)) {
			float nonScaledPrimalRadius = nonscaledPrimalRect.getWidth() / 2.0f;
			float scaledPrimalRadius = nonScaledPrimalRadius * radiusScaleFactor;
			float nonScaledX = nonscaledPrimalRect.getLeft() + nonScaledPrimalRadius;
			float nonScaledY = nonscaledPrimalRect.getTop() + nonScaledPrimalRadius;

			Rect result = new Rect( nonScaledX - scaledPrimalRadius, nonScaledY
					- scaledPrimalRadius, scaledPrimalRadius * 2.0f,
					scaledPrimalRadius * 2.0f);
			return result;
		}
		return null;
	}


	public void setLeftmost( Orbit orbit) {
		leftmost = orbit;
	}


	public void setRightmost( Orbit orbit) {
		rightmost = orbit;
	}


	public void setTopmost( Orbit orbit) {
		topmost = orbit;
	}


	public void setBottommost( Orbit orbit) {
		bottommost = orbit;
	}


	@Override
	public boolean updateToAnRelatedOrbit( Orbit theRelatedOrbit) {
		boolean doUpdateAgain = false;
		CancerDisc theOtherDisc = (CancerDisc) theRelatedOrbit;
		boolean isIntersected = intersectWith( theOtherDisc);
		if ( isIntersected) {
			float thisNonscaledPrimalRadius = nonscaledPrimalRect.getWidth() / 2.0f;
			float thisNonscaledX = nonscaledPrimalRect.getLeft()
					+ thisNonscaledPrimalRadius;
			float thisNonscaledY = nonscaledPrimalRect.getTop()
					+ thisNonscaledPrimalRadius;
			float theOtherDiscNonscaledPrimalRadius = theOtherDisc.nonscaledPrimalRect
					.getWidth() / 2.0f;
			float theOtherDiscNonscaledX = theOtherDisc.nonscaledPrimalRect.getLeft()
					+ theOtherDiscNonscaledPrimalRadius;
			float theOtherDiscNonscaledY = theOtherDisc.nonscaledPrimalRect.getTop()
					+ theOtherDiscNonscaledPrimalRadius;

			float dist = distanceBetween( thisNonscaledX, thisNonscaledY,
					theOtherDiscNonscaledX, theOtherDiscNonscaledY);
			if ( new Float( 0).equals( thisNonscaledPrimalRadius
					+ theOtherDiscNonscaledPrimalRadius)
					|| new Float( 0).equals( dist)) {
				return false;
			}
			float newRadiusScaleFactor = ( dist / ( thisNonscaledPrimalRadius + theOtherDiscNonscaledPrimalRadius));
			if ( newRadiusScaleFactor != radiusScaleFactor) {
				radiusScaleFactor = newRadiusScaleFactor;
				doUpdateAgain = true;
			}
		}

		return doUpdateAgain;
	}


	public Rect getRect() {
		return new Rect( getPosition(), getWidth(), getHeight());
	}


	public void setRect( Rect rect) {
		setPosition( rect.getLeft(), rect.getTop());
		float len = Math.min( rect.getWidth(), rect.getHeight());
		setWidth( (int) len);
		setHeight( (int) len);
		this.radius = len / 2.0f;

		// fit radius
		float thisNonscaledPrimalRadius = nonscaledPrimalRect.getWidth() / 2.0f;
		if ( thisNonscaledPrimalRadius * radiusScaleFactor > this.radius)
			radiusScaleFactor = this.radius / thisNonscaledPrimalRadius;

		this.x = rect.getLeft() + radius;
		this.y = rect.getTop() + radius;
		this.update();
	}


	public static void resetRadiusScaleFactor() {
		radiusScaleFactor = 100f;
	}


	private void readjustInterleaving() {
		int w = parent().getWidth();
		int h = parent().getHeight();

		if ( x + radius > w)
			x = w - radius;

		if ( x - radius < 0)
			x = radius;

		if ( y + radius > h)
			y = h - radius;

		if ( y - radius < 0)
			y = radius;
	}


	private boolean interleavedWindow() {
		return ( ( x + radius > width) || ( x - radius > 0)
				|| ( y + radius > height) || ( y - radius > 0));
	}


	private boolean intersectWith( CancerDisc theOtherDisc) {
		return ( this.radius + theOtherDisc.radius) >= distanceBetween( this.x,
				this.y, theOtherDisc.x, theOtherDisc.y);
	}


	private float distanceBetween( float px, float py, float qx, float qy) {
		double dx = qx - px;
		double dy = qy - py;
		return (float) Math.sqrt( dx * dx + dy * dy);
	}


	@Override
	public Controller setValue( float theValue) {
		// TODO Auto-generated method stub
		return null;
	}


	@Override
	public void draw( PApplet theApplet) {
		if ( Test.isAssigned( theApplet)) {

			PApplet p = theApplet;
			p.pushStyle();

			RingSegmentStyle[] ringSegmentStyles = null;
			PStyle nextStyle;
			int segmentSolution = 32;
			int angleCorrection = segmentSolution / 4 * 3;

			float outerCancerIncidenceRing = p.max( radius, NUM_MIN_CIRCLE_RADIUS);
			float innerCancerMortalityRing = outerCancerIncidenceRing > NUM_MIN_CIRCLE_RADIUS ? data.mortalityASR
					/ data.incidenceASR * outerCancerIncidenceRing
					: 0.0f;

			switch ( cancerCause) {
				case AIR_POLLUTION:
					float greyScaleColorOfAirPollution = p.map(
							dataCancerCauses.airPollutionExposure.airPollution, 0,
							dataMaxAirPollutionExposure, 200, 0);
					nextStyle = p.g.getStyle();
					nextStyle.fillColor = p.color( greyScaleColorOfAirPollution);
					nextStyle.strokeColor = p.color( 255, 127);
					ringSegmentStyles = RingSegmentStyle.add( ringSegmentStyles,
							nextStyle, 0);
					break;
				case UV_RADIATION:
					float scaleColorOfUVRadiation = p.map(
							dataCancerCauses.uvRadiationExposure.uvRadiation, 0,
							dataMaxUVRadiationExposure, 0, 1);
					int yellowScaleColorOfUVRadiation = p.lerpColor(
							p.color( 200), p.color( 255, 255, 0), scaleColorOfUVRadiation);
					nextStyle = p.g.getStyle();
					nextStyle.fillColor = yellowScaleColorOfUVRadiation;
					nextStyle.strokeColor = p.color( 255, 127);
					ringSegmentStyles = RingSegmentStyle.add( ringSegmentStyles,
							nextStyle, 0);
					break;
				case AGE:
					// TODO: define colors as constants
					int childAgeColor = p.color( 5, 166, 164);
					int midAgeColor = p.color( 242, 198, 56);
					int grandAgeColor = p.color( 75, 0, 70);

					p.pushStyle();
					p.stroke( p.color( 255, 127));
					nextStyle = p.g.getStyle();
					nextStyle.fillColor = childAgeColor;
					nextStyle.strokeColor = p.color( 255, 127);
					ringSegmentStyles = RingSegmentStyle.add( ringSegmentStyles,
							nextStyle, angleCorrection);
					nextStyle = p.g.getStyle();
					nextStyle.fillColor = midAgeColor;
					ringSegmentStyles = RingSegmentStyle
							.add(
									ringSegmentStyles,
									nextStyle,
									( angleCorrection - segmentSolution)
											+ p.round( dataCancerCauses.ageSpreading.percentageDistributionPerAgeClasses[0]
													* segmentSolution / 100));
					nextStyle = p.g.getStyle();
					nextStyle.fillColor = grandAgeColor;
					ringSegmentStyles = RingSegmentStyle
							.add(
									ringSegmentStyles,
									nextStyle,
									( angleCorrection - segmentSolution)
											+ p.round( dataCancerCauses.ageSpreading.percentageDistributionPerAgeClasses[1]
													* segmentSolution / 100));
					break;
				case SEX:
				default:
					// TODO: define colors as constants
					int maleAgeColor = p.color( 86, 186, 236);
					int femaleAgeColor = p.color( 245, 64, 119);
					p.pushStyle();
					p.stroke( p.color( 255, 127));
					nextStyle = p.g.getStyle();
					nextStyle.fillColor = maleAgeColor;
					nextStyle.strokeColor = p.color( 255, 127);
					ringSegmentStyles = RingSegmentStyle.add( ringSegmentStyles,
							nextStyle, angleCorrection);
					nextStyle = p.g.getStyle();
					nextStyle.fillColor = femaleAgeColor;
					ringSegmentStyles = RingSegmentStyle.add(
							ringSegmentStyles,
							nextStyle,
							( angleCorrection - segmentSolution)
									+ p.round( dataCancerCauses.sexSpreading.malesPer100
											* segmentSolution / 100));
					break;
			}

			// draw the ring
			Graphics.ring( p, (int) this.x, (int) this.y, outerCancerIncidenceRing,
					innerCancerMortalityRing, segmentSolution, ringSegmentStyles);

			if ( outerCancerIncidenceRing > NUM_MIN_CIRCLE_RADIUS) {
				drawCancerDimensionArrows( p, outerCancerIncidenceRing,
						innerCancerMortalityRing);
			}

			if ( !isShowCancerDimensionArrows) {
				// print the location label
				float locationNameWidth = p.textWidth( data.locationName);
				if ( innerCancerMortalityRing * 2 > locationNameWidth) {
					p.fill( p.color( 255));
					float tx = this.x - ( locationNameWidth / 2);
					float ty = this.y + 5;
					p.text( data.locationName, tx, ty);
				}
			}

			if ( isMouseOver() == true) {
				mouseOverEvent( p);
			}

			draw2DCoordScalaValue( p);

			p.popStyle();
		}
	}


	private boolean isMouseBetweenTheRings( PApplet p, float outerRadius,
			float innerRadius) {
		return isMouseInRing( p, outerRadius) && !isMouseInRing( p, innerRadius);
	}


	private boolean isMouseInRing( PApplet p, float comparativeRadius) {
		float mouseRadius = distanceBetween( p.mouseX - parent.getPosition().x,
				p.mouseY - parent.getPosition().y, this.x, this.y);
		return !( mouseRadius > comparativeRadius);
	}


	@Override
	protected void onEnter() {
		super.onEnter();
	}


	private void drawCrosshairs( PApplet p) {
		p.pushStyle();
		int locationLabelWidth = (int) p.textWidth( getLocationName());
		p.stroke( 255, 111);
		p.line( this.x, 0 + HealthGraph.NUM_SCALE_MARGIN - 5, this.x, this.y - 10);
		p.line( this.x, this.y + 10, this.x, getParent().getHeight()
				- HealthGraph.NUM_SCALE_MARGIN + 5);
		p.line( HealthGraph.NUM_SCALE_MARGIN - 5, this.y, this.x
				- ( locationLabelWidth / 2) - 5, this.y);
		p.line( this.x + ( locationLabelWidth / 2) + 5, this.y, getParent()
				.getWidth() - HealthGraph.NUM_SCALE_MARGIN + 5, this.y);
		p.popStyle();
	}


	private void drawCancerDimensionArrows( PApplet p, float outerRadius,
			float innerRadius) {
		isShowCancerDimensionArrows = isMouseBetweenTheRings( p, outerRadius,
				innerRadius);
		if ( isShowCancerDimensionArrows) {
			p.pushMatrix();
			p.pushStyle();
			p.textSize( 9);
			p.translate( this.x, this.y);
			p.rotate( -PI / 4);
			p.stroke( 255);
			p.fill( 255);

			// Bemaßungspfeil äußerer Radius (cancer incidence)
			p.line( -outerRadius - 60, 0, -outerRadius, 0);
			p.triangle( -outerRadius, 0, -outerRadius - 5, -3, -outerRadius - 5, 3);
			p.text( String.format( "%.02f ASR", data.incidenceASR),
					-outerRadius - 60, -5);
			p.text( "incidence", -outerRadius - 60, 12);

			// Bemaßungspfeil innerer Radius (cancer mortality)
			p.line( -innerRadius, 0, -innerRadius + 60, 0);
			p.triangle( -innerRadius, 0, -innerRadius + 5, -3, -innerRadius + 5, 3);
			p.text( String.format( "%.02f ASR", data.mortalityASR),
					-innerRadius + 10, -5);
			p.text( "mortality", -innerRadius + 10, 12);

			p.popStyle();
			p.popMatrix();
		}
	}


	private void draw2DCoordScalaValue( PApplet p) {
		boolean drawHorizScala = false;
		boolean drawVertScala = false;
		boolean boldText = false;

		if ( isMouseOver() && isInside() && !isShowCancerDimensionArrows) {
			boldText = true;
			drawHorizScala = true;
			drawVertScala = true;
		} else {
			if ( leftmost.equals( rightmost) ||
					// es gibt nur eine CancerDisk, und das muss diese sein
					leftmost.equals( this) ||
					// diese Disk ist die am weitesten links befindliche
					rightmost.equals( this)) {
				// diese Disk ist die am weitesten rechts befindliche
				drawHorizScala = true;
			} 
			if ( topmost.equals( bottommost) ||
					// es gibt nur eine CancerDisk, und das muss diese sein
					topmost.equals( this) ||
					// diese Disk ist die am weitesten links befindliche
					bottommost.equals( this)) {
				// diese Disk ist die am weitesten rechts befindliche
				drawVertScala = true;
			}
		}

		if ( drawVertScala || drawHorizScala) {
			p.pushStyle();

			if ( boldText) {
				p.fill( 255);
				p.textSize( 14);
			} else {
				p.fill( 255, 111);
				p.textSize( 10);
			}

			if ( drawHorizScala) {

				String healthFinancingScalaValue = String.format( " %.1f ",
						this.data.healthFinancing);
				int lenValueX = (int) p.textWidth( healthFinancingScalaValue);
				p.text( healthFinancingScalaValue, this.x - ( lenValueX / 2), 12);
				p.text( healthFinancingScalaValue, this.x - ( lenValueX / 2), parent()
						.getHeight() -2);

			}

			if ( drawVertScala ) {
				String lifeExpectancyScalaValue = String.format( " %d ",
						this.data.lifeExpectancy);
				int lenValueY = (int) p.textWidth( lifeExpectancyScalaValue);
				p.pushMatrix();
				p.translate( parent().getWidth() / 2.0f, parent().getHeight() / 2.0f);
				p.rotate( -PI / 2);
				float thisMappedY = -( ( lenValueY / 2) + p.map( this.y, 0, parent()
						.getHeight(), -parent().getHeight() / 2.0f,
						parent().getHeight() / 2.0f));
				p.text( lifeExpectancyScalaValue, thisMappedY, 12 - ( parent()
						.getWidth() / 2.0f));
				p.text( lifeExpectancyScalaValue, thisMappedY,
						parent().getWidth() / 2.0f -2);
				p.popMatrix();
			}
			
			p.popStyle();
		}
	}


	private void mouseOverEvent( PApplet p) {
		if ( this.isInside() && !isShowCancerDimensionArrows) {
			drawCrosshairs( p);
		}
	}


	@Override
	protected void mousePressed() {

		( (CustomTooltip) controlP5.getTooltip()).activate( this);
		super.mousePressed();
	}


	private void drawToolTip( PApplet p) {
		p.pushStyle();
		p.fill( 255, 111);
		p.rectMode( p.CORNER);
		p.rect( p.mouseX - parent().getPosition().x, p.mouseY
				- parent().getPosition().y, 200, 300);
		// // p.text( data.locationName, tx, ty);
		p.popStyle();
	}

}
